package in.lib.utils.html; import in.lib.view.spannable.HashtagClickableSpan; import in.lib.view.spannable.MarkDownClickableSpan; import in.lib.view.spannable.MentionClickableSpan; import in.lib.view.spannable.UrlClickableSpan; import java.io.IOException; import java.io.StringReader; import org.ccil.cowan.tagsoup.HTMLSchema; import org.ccil.cowan.tagsoup.Parser; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import android.graphics.Typeface; import android.text.Html; import android.text.Html.ImageGetter; import android.text.Html.TagHandler; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ParagraphStyle; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; import android.text.style.SuperscriptSpan; import android.text.style.UnderlineSpan; public class ADNHtml { // can't inherit android.text.Html, the constructor is private private static class HtmlParser { private static final HTMLSchema schema = new HTMLSchema(); } public static Spanned fromHtml(String source) { return fromHtml(source, null, null); } public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { Parser parser = new Parser(); try { parser.setProperty(Parser.schemaProperty, HtmlParser.schema); } catch (org.xml.sax.SAXNotRecognizedException e) { // Should not happen. throw new RuntimeException(e); } catch (org.xml.sax.SAXNotSupportedException e) { // Should not happen. throw new RuntimeException(e); } HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser); return converter.convert(); } } class HtmlToSpannedConverter implements ContentHandler { private final String mSource; private final XMLReader mReader; private final SpannableStringBuilder mSpannableStringBuilder; private final Html.TagHandler mTagHandler; public HtmlToSpannedConverter(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser) { mSource = source; mSpannableStringBuilder = new SpannableStringBuilder(); mTagHandler = tagHandler; mReader = parser; } public Spanned convert() { mReader.setContentHandler(this); try { mReader.parse(new InputSource(new StringReader(mSource))); } catch (IOException e) { // We are reading from a string. There should not be IO problems. throw new RuntimeException(e); } catch (SAXException e) { // TagSoup doesn't throw parse exceptions. throw new RuntimeException(e); } // Fix flags and range for paragraph-type markup. Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); for (int i = 0; i < obj.length; i++) { int start = mSpannableStringBuilder.getSpanStart(obj[i]); int end = mSpannableStringBuilder.getSpanEnd(obj[i]); // If the last line of the range is blank, back off by one. if (end - 2 >= 0) { if (mSpannableStringBuilder.charAt(end - 1) == '\n' && mSpannableStringBuilder.charAt(end - 2) == '\n') { end--; } } if (end == start) { mSpannableStringBuilder.removeSpan(obj[i]); } else { mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); } } return mSpannableStringBuilder; } private void handleStartTag(String tag, Attributes attributes) { if (tag.equalsIgnoreCase("strong")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("b")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("em")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("i")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("a")) { startA(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("md")) { startMd(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("u")) { start(mSpannableStringBuilder, new Underline()); } else if (tag.equalsIgnoreCase("sub")) { start(mSpannableStringBuilder, new Sub()); } else if (tag.equalsIgnoreCase("span") && attributes.getValue("", "itemprop") != null) { if (attributes.getValue("", "itemprop").equals("mention")) { startADNMention(mSpannableStringBuilder, attributes); } else if (attributes.getValue("", "itemprop").equals("hashtag")) { startADNHashtag(mSpannableStringBuilder, attributes); } } else if (mTagHandler != null) { mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); } } private void handleEndTag(String tag) { if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("strong")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("b")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("em")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("i")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("a")) { endADNTag(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("md")) { endADNTag(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("u")) { end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); } else if (tag.equalsIgnoreCase("sup")) { end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); } else if (tag.equalsIgnoreCase("sub")) { end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); } else if (tag.equalsIgnoreCase("span")) { endADNTag(mSpannableStringBuilder); } else if (mTagHandler != null) { mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); } } private static void handleBr(SpannableStringBuilder text) { text.append("\n"); } private static Object getLast(Spanned text, Class kind) { /* * This knows that the last returned object from getSpans() will be the * most recently added. */ Object[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } else { return objs[objs.length - 1]; } } private static void start(SpannableStringBuilder text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); } private static void end(SpannableStringBuilder text, Class kind, Object repl) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } return; } private static void startADNMention(SpannableStringBuilder text, Attributes attributes) { String username = attributes.getValue("", "data-mention-name"); String userid = attributes.getValue("", "data-mention-id"); int len = text.length(); text.setSpan(new Mention(username, userid), len, len, Spannable.SPAN_MARK_MARK); } private static void startADNHashtag(SpannableStringBuilder text, Attributes attributes) { String tag = attributes.getValue("", "data-hashtag-name"); int len = text.length(); text.setSpan(new Hashtag(tag), len, len, Spannable.SPAN_MARK_MARK); } private static void startA(SpannableStringBuilder text, Attributes attributes) { String href = attributes.getValue("", "href"); int len = text.length(); text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK); } private static void startMd(SpannableStringBuilder text, Attributes attributes) { String href = attributes.getValue("", "href"); String anchor = attributes.getValue("", "data-anchor"); int len = text.length(); text.setSpan(new MDLink(href, anchor), len, len, Spannable.SPAN_MARK_MARK); } private static void endADNTag(SpannableStringBuilder text) { int len = text.length(); Object obj = getLast(text, ADNTag.class); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { if (obj instanceof Mention) { Mention m = (Mention)obj; text.setSpan(new MentionClickableSpan(m.mUsername, m.mUserid), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (obj instanceof Hashtag) { Hashtag h = (Hashtag)obj; text.setSpan(new HashtagClickableSpan(h.mTag), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (obj instanceof Href) { Href h = (Href)obj; text.setSpan(new UrlClickableSpan(h.mHref), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (obj instanceof MDLink) { MDLink h = (MDLink)obj; text.setSpan(new MarkDownClickableSpan(h.mHref, h.text), where, where + h.text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } @Override public void setDocumentLocator(Locator locator) { } @Override public void startDocument() throws SAXException { } @Override public void endDocument() throws SAXException { } @Override public void startPrefixMapping(String prefix, String uri) throws SAXException { } @Override public void endPrefixMapping(String prefix) throws SAXException { } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { handleStartTag(localName, attributes); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { handleEndTag(localName); } @Override public void characters(char ch[], int start, int length) throws SAXException { StringBuilder sb = new StringBuilder(); /* * Ignore whitespace that immediately follows other whitespace; newlines * count as spaces. */ for (int i = 0; i < length; i++) { char c = ch[i + start]; if (c == ' ' || c == '\n') { char pred; int len = sb.length(); if (len == 0) { len = mSpannableStringBuilder.length(); if (len == 0) { pred = '\n'; } else { pred = mSpannableStringBuilder.charAt(len - 1); } } else { pred = sb.charAt(len - 1); } if (pred != ' ' && pred != '\n') { sb.append(' '); } } else { sb.append(c); } } mSpannableStringBuilder.append(sb); } @Override public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { } @Override public void processingInstruction(String target, String data) throws SAXException { } @Override public void skippedEntity(String name) throws SAXException { } private static class Bold{} private static class Italic{} private static class Underline{} private static class Super{} private static class Sub{} private static class ADNTag{} private static class Href extends ADNTag { public String mHref; public Href(String href) { mHref = href; } } private static class MDLink extends ADNTag { public String mHref; public String text; public MDLink(String href, String text) { mHref = href; this.text = text; } } private static class Mention extends ADNTag { public String mUsername; public String mUserid; public Mention(String username, String userid) { mUsername = username; mUserid = userid; } } private static class Hashtag extends ADNTag { public String mTag; public Hashtag(String tag) { mTag = tag; } } }